﻿//using Microsoft.Vbe.Interop;
///using 枸杞VBE插件.代码格式化;
//using 枸杞VBE插件.代码格式化.SafeComWrappers;
//using Microsoft.Vbe.Interop;
using NetOffice.VBIDEApi;
using System;
using System.Collections.Generic;
using System.Linq;

namespace 枸杞VBE插件.代码格式化
{
    public class Indenter
    {
        private readonly VBE _vbe;
        private readonly Func<IIndenterSettings> _settings;

        public Indenter(VBE vbe)//, Func<IIndenterSettings> settings)
        {
            _vbe = vbe;
            _settings = Properties.Settings.Default.GetSettings;
        }

        /// <summary>
        /// Indents the procedure selected in the ActiveCodePane. If more than one is selected, the first is indented.
        /// </summary>
        public void IndentCurrentProcedure()
        {
            var pane = _vbe.ActiveCodePane;
            if (pane == null) return; var initialSelection = pane.GetSelection().Collapse();
            var module = pane.CodeModule;
            var selection = pane.GetSelection();
            var procName = module.ProcOfLine(selection.StartLine, out NetOffice.VBIDEApi.Enums.vbext_ProcKind procKind);
            if (string.IsNullOrEmpty(procName)) return;
            var startLine = module.ProcStartLine(procName, procKind);
            var endLine = startLine + module.ProcCountLines(procName, procKind);
            selection = new Selection(startLine, 1, endLine, 1);
            var component = module.Parent;
            Indent(component, selection, true);
            ResetSelection(pane, initialSelection);
        }

        /// <summary>
        /// Indents the code in the ActiveCodePane.
        /// </summary>
        public void IndentCurrentModule()
        {
            var pane = _vbe.ActiveCodePane;
            if (pane == null) return;
            var initialSelection = pane.GetSelection().Collapse();
            Indent(pane.CodeModule.Parent);
            ResetSelection(pane, initialSelection);
        }

        /// <summary>
        /// Indents every module in the active project.
        /// </summary>
        public void IndentCurrentProject()
        {
            var pane = _vbe.ActiveCodePane;
            var initialSelection = pane == null ? default : pane.GetSelection().Collapse();
            var project = _vbe.ActiveVBProject;
            if (project.Protection == NetOffice.VBIDEApi.Enums.vbext_ProjectProtection.vbext_pp_locked) return;
            foreach (VBComponent component in project.VBComponents)
            {
                Indent(component);
            }
            ResetSelection(pane, initialSelection);
        }

        private void ResetSelection(CodePane codePane, Selection initialSelection)
        {
            var window = _vbe.ActiveWindow;
            if (initialSelection == default || codePane == null || window == null || window.Type != NetOffice.VBIDEApi.Enums.vbext_WindowType.vbext_wt_CodeWindow) return;
            var module = codePane.CodeModule;
            // This will only "ballpark it" for now - it sets the absolute line in the module, not necessarily
            // the specific LoC. That will be a TODO when the parse tree is used to indent. For the time being,
            // maintaining that is ridiculously difficult vis-a-vis the payoff if the vertical spacing is
            // changed.
            var lines = module.CountOfLines;
            if (lines < initialSelection.StartLine)
            {
                codePane.SetSelection(new Selection(lines, initialSelection.StartColumn, lines, initialSelection.StartColumn));
            }
            else
            {
                codePane.SetSelection(initialSelection);
            }
        }

        /// <summary>
        /// Indents the code in the VBComponent's CodeModule.
        /// </summary>
        /// <param name="component">The VBComponent to indent</param>
        public void Indent(VBComponent component)
        {
            var module = component.CodeModule;
            var lineCount = module.CountOfLines;
            if (lineCount == 0) return;
            var codeLines = module.Lines(1, lineCount).Replace("\r", string.Empty).Split('\n');
            var indented = Indent(codeLines, true);
            module.DeleteLines(1, lineCount);
            module.InsertLines(1, string.Join("\r\n", indented));
        }

        /// <summary>
        /// Not fully implemented for selections (it does not track the current indentation level before the call). Use at your own
        /// risk on anything smaller than a procedure - the caller is responsible for determining the base indent and restoring it
        /// *after* the call.
        /// </summary>
        /// <param name="component">The VBComponent to indent</param>
        /// <param name="selection">The selection to indent</param>
        /// <param name="procedure">Whether the selection is a single procedure</param>
        private void Indent(VBComponent component, Selection selection, bool procedure = false)
        {
            var module = component.CodeModule;
            var lineCount = module.CountOfLines;
            if (lineCount == 0) return;
            var codeLines = module.Lines(selection.StartLine, selection.LineCount).Replace("\r", string.Empty).Split('\n');
            var indented = Indent(codeLines, false, procedure);
            var start = selection.StartLine;
            var lines = selection.LineCount;
            //Deletelines fails if the the last line of the procedure is the last line of the module.
            module.DeleteLines(start, start + lines < lineCount ? lines : lines - 1);
            module.InsertLines(start, string.Join("\r\n", indented));
        }

        private IEnumerable<LogicalCodeLine> BuildLogicalCodeLines(IEnumerable<string> lines, out IIndenterSettings settings)
        {
            settings = _settings.Invoke();
            var logical = new List<LogicalCodeLine>();
            LogicalCodeLine current = null;
            AbsoluteCodeLine previous = null;
            foreach (var line in lines)
            {
                var absolute = new AbsoluteCodeLine(line, settings, previous);
                if (current == null)
                {
                    current = new LogicalCodeLine(absolute, settings);
                    logical.Add(current);
                }
                else
                {
                    current.AddContinuationLine(absolute);
                }
                if (!absolute.HasContinuation)
                {
                    current = null;
                }
                previous = absolute;
            }
            return logical;
        }

        /// <summary>
        /// Indents the code contained in the passed string. NOTE: This overload should only be used on procedures or modules.
        /// </summary>
        /// <param name="code">The code block to indent</param>
        /// <returns>Indented code lines</returns>
        public IEnumerable<string> Indent(string code)
        {
            return Indent(code.Replace("\r", string.Empty).Split('\n'), false);
        }

        /// <summary>
        /// Indents a range of code lines. NOTE: If inserting procedures, use the forceTrailingNewLines overload to preserve vertical spacing in the module.
        /// Do not call directly on selections. Use Indent(IVBComponent, Selection) instead.
        /// </summary>
        /// <param name="codeLines">Code lines to indent</param>
        /// <returns>Indented code lines</returns>
        public IEnumerable<string> Indent(IEnumerable<string> codeLines)
        {
            return Indent(codeLines, false);
        }

        /// <summary>
        /// Indents a range of code lines. Do not call directly on selections. Use Indent(IVBComponent, Selection) instead.
        /// </summary>
        /// <param name="codeLines">Code lines to indent</param>
        /// <param name="forceTrailingNewLines">If true adds a number of blank lines after the last procedure based on VerticallySpaceProcedures settings</param>
        /// <returns>Indented code lines</returns>
        public IEnumerable<string> Indent(IEnumerable<string> codeLines, bool forceTrailingNewLines)
        {
            return Indent(codeLines, forceTrailingNewLines, false);
        }

        private IEnumerable<string> Indent(IEnumerable<string> codeLines, bool forceTrailingNewLines, bool procedure)
        {
            var logical = BuildLogicalCodeLines(codeLines, out var settings).ToList();
            var indents = 0;
            var start = false;
            var enumStart = false;
            var inEnumType = false;
            var inProcedure = false;
            foreach (var line in logical)
            {
                inEnumType &= !line.IsEnumOrTypeEnd;
                if (inEnumType)
                {
                    line.AtEnumTypeStart = enumStart;
                    enumStart = line.IsCommentBlock;
                    line.IsEnumOrTypeMember = true;
                    line.InsideProcedureTypeOrEnum = true;
                    line.IndentationLevel = line.EnumTypeIndents;
                    continue;
                }
                if (line.IsProcedureStart)
                {
                    inProcedure = true;
                }
                line.InsideProcedureTypeOrEnum = inProcedure || enumStart;
                inProcedure = inProcedure && !line.IsProcedureEnd && !line.IsEnumOrTypeEnd;
                if (line.IsProcedureStart || line.IsEnumOrTypeStart)
                {
                    indents = 0;
                }
                line.AtProcedureStart = start;
                line.IndentationLevel = indents - line.Outdents;
                indents += line.NextLineIndents;
                start = line.IsProcedureStart ||
                        line.AtProcedureStart && line.IsDeclaration ||
                        line.AtProcedureStart && line.IsCommentBlock ||
                        settings.IgnoreEmptyLinesInFirstBlocks && line.AtProcedureStart && line.IsEmpty;
                inEnumType = line.IsEnumOrTypeStart;
                enumStart = inEnumType;
            }
            return GenerateCodeLineStrings(logical, forceTrailingNewLines, procedure);
        }

        private IEnumerable<string> GenerateCodeLineStrings(IEnumerable<LogicalCodeLine> logical, bool forceTrailingNewLines, bool procedure = false)
        {
            var output = new List<string>();
            var settings = _settings.Invoke();
            List<LogicalCodeLine> indent;
            if (!procedure && settings.VerticallySpaceProcedures)
            {
                indent = new List<LogicalCodeLine>();
                var lines = logical.ToArray();
                var header = true;
                var inEnumType = false;
                var propertyGroupIdentifier = string.Empty;
                for (var i = 0; i < lines.Length; i++)
                {
                    indent.Add(lines[i]);
                    if (header && lines[i].IsEnumOrTypeStart)
                    {
                        inEnumType = true;
                    }
                    if (header && lines[i].IsEnumOrTypeEnd)
                    {
                        inEnumType = false;
                    }
                    if (header && !inEnumType && lines[i].IsProcedureStart)
                    {
                        header = false;
                        SpaceHeader(indent, settings);
                        propertyGroupIdentifier = lines[i].IsPropertyStart
                            ? ExtractPropertyIdentifier(lines[i].ToString())
                            : string.Empty;
                        continue;
                    }
                    if (!lines[i].IsEnumOrTypeEnd && !lines[i].IsProcedureEnd)
                    {
                        continue;
                    }
                    while (++i < lines.Length && lines[i].IsEmpty) { }
                    if (i != lines.Length)
                    {
                        var linesBetweenProcedures = settings.LinesBetweenProcedures;
                        if (lines[i].IsPropertyStart)
                        {
                            var propertyIdentifier = ExtractPropertyIdentifier(lines[i].ToString());
                            if (propertyIdentifier.Equals(propertyGroupIdentifier, StringComparison.InvariantCultureIgnoreCase)
                                && settings.GroupRelatedProperties)
                            {
                                linesBetweenProcedures = 0;
                            }
                            else
                            {
                                propertyGroupIdentifier = propertyIdentifier;
                            }
                        }
                        if (linesBetweenProcedures > 0)
                        {
                            indent.Add(new LogicalCodeLine(Enumerable.Repeat(new AbsoluteCodeLine(string.Empty, settings), linesBetweenProcedures), settings));
                        }
                        indent.Add(lines[i]);
                    }
                    else if (forceTrailingNewLines && i == lines.Length)
                    {
                        indent.Add(new LogicalCodeLine(Enumerable.Repeat(new AbsoluteCodeLine(string.Empty, settings), Math.Max(settings.LinesBetweenProcedures, 1)), settings));
                    }
                }
            }
            else
            {
                indent = logical.ToList();
            }
            foreach (var line in indent)
            {
                output.AddRange(line.Indented().Split(new[] { Environment.NewLine }, StringSplitOptions.None));
            }
            return output;
        }

        private static string ExtractPropertyIdentifier(string line)
        {
            var declarationElementsStartingAtGetLetOrSet = line.ToString().Split(' ').SkipWhile(c => !c.EndsWith("et")).ToArray();
            return declarationElementsStartingAtGetLetOrSet[1].Split(new string[] { "(" }, StringSplitOptions.None).FirstOrDefault();
        }

        private static void SpaceHeader(IList<LogicalCodeLine> header, IIndenterSettings settings)
        {
            var commentSkipped = false;
            var commentLines = 0;
            for (var i = header.Count - 2; i >= 0; i--)
            {
                if (!commentSkipped && header[i].IsCommentBlock)
                {
                    commentLines++;
                    continue;
                }
                commentSkipped = true;
                if (header[i].IsEmpty)
                {
                    header.RemoveAt(i);
                }
                else
                {
                    header.Insert(header.Count - 1 - commentLines,
                        new LogicalCodeLine(
                            Enumerable.Repeat(new AbsoluteCodeLine(string.Empty, settings),
                                settings.LinesBetweenProcedures), settings));
                    return;
                }
            }
        }
    }
}